package cn.demoncat.util.mysql.util;

import cn.demoncat.util.exception.IoRuntimeException;
import cn.demoncat.util.io.CloseUtil;
import cn.demoncat.util.io.FileUtil;
import cn.demoncat.util.io.FreeMarkerUtil;
import cn.demoncat.util.io.ZipUtil;
import cn.demoncat.util.lang.JDBCUtil;
import cn.demoncat.util.lang.LocalDateUtil;
import cn.demoncat.util.lang.constant.StatusConstant.Common;
import cn.demoncat.util.lang.constant.StringConstant;
import cn.demoncat.util.lang.entity.Result;
import cn.demoncat.util.mysql.constant.DbGenConstant.BaseType;
import cn.demoncat.util.mysql.entity.*;
import cn.demoncat.util.web.ServletUtil;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;

/**
 * Mybatis代码生成工具
 * 
 * @author 延晓磊
 *
 * @since 2017年9月12日
 */
public class MybatisCodeGenUtil {
 
	// 模板路径
	private static final String TEMPLATE_DIR = "/ftl/ssm";
	// 临时目录
	private static final String TMP_DER = "/tmp/";
		
	// BASE包名
	private static final String BASE_PACKAGE = "cn.demoncat.util.mybatis.base";
	// MapperOgnl全类名
	public static final String MAPPER_OGNL = "cn.demoncat.util.mybatis.util.MapperOgnl";
	// DAO后缀：Mapper或Dao
	public static final String MAPPER_SUFFIX = "Mapper";
	
	private static final Logger LOG = LoggerFactory.getLogger(MybatisCodeGenUtil.class);

	/**
	 * 本地生成代码（宽松的：不校验id、注释）
	 *
	 * @param out			输出目录，后缀/
	 * @param db			数据库
	 * @param tableName		表名：* 表示全部，xx* 前缀匹配，xx 单表，xx,yy 多表
	 * @param author		作者
	 * @param baseType		根类型："" 普通数据 | Tree 树型数据 | Report 报表数据
	 * @param classNamePre	类名前缀，区分多数据源的同名表："" | Xxx
	 * @param packageName	包名前缀：cn.xxx.xxx
	 *
	 * @author 延晓磊
	 *
	 * @since 2019年7月3日
	 */
	public static void localGenLoose(String out, DbGenParam db,String tableName,String author,String baseType,String classNamePre,String packageName) {
		localGen(out, db, tableName, author, baseType, classNamePre, packageName, MAPPER_OGNL, MAPPER_SUFFIX, false);
	}

	/**
	 * 本地生成代码
	 * 
	 * @param out			输出目录，后缀/
	 * @param db			数据库
	 * @param tableName		表名：* 表示全部，xx* 前缀匹配，xx 单表，xx,yy 多表
	 * @param author		作者
	 * @param baseType		根类型："" 普通数据 | Tree 树型数据 | Report 报表数据
	 * @param classNamePre	类名前缀，区分多数据源的同名表："" | Xxx
	 * @param packageName	包名前缀：cn.xxx.xxx
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年7月3日
	 */
	public static void localGen(String out, DbGenParam db,String tableName,String author,String baseType,String classNamePre,String packageName) {
		localGen(out, db, tableName, author, baseType, classNamePre, packageName, MAPPER_OGNL, MAPPER_SUFFIX, true);
	}
	
	/**
	 * 本地生成代码
	 * 
	 * @param out			输出目录，后缀/
	 * @param db			数据库
	 * @param tableName		表名：* 表示全部，xx* 前缀匹配，xx 单表，xx,yy 多表
	 * @param author		作者
	 * @param baseType		根类型："" 普通数据 | Tree 树型数据 | Report 报表数据
	 * @param classNamePre	类名前缀，区分多数据源的同名表："" | Xxx
	 * @param packageName	包名前缀：cn.xxx.xxx
	 * @param mapperOgnl	Mapper.xml的OGNL类名：cn.demoncat.util.mybatis.util.MapperOgnl
	 * @param mapperSuffix	Mapper类的后缀：Dao | Mapper
	 * @param strict		严格的：校验id、注释
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年7月3日
	 */
	public static void localGen(String out, DbGenParam db,String tableName,String author,String baseType,String classNamePre,String packageName,String mapperOgnl,String mapperSuffix, boolean strict) {
		if (!out.endsWith(StringConstant.SEPARATOR)) {
			out = out + StringConstant.SEPARATOR;
		}
		// 查询数据库表
		List<MysqlTable> tableInfos = DbGenUtil.listTableInfos(db, DbGenUtil.listTables(db, tableName));
		// 生成代码
		OutputStream output = null;
		try {
			output = new FileOutputStream(out + db.getName() + ".zip");
			gen(db, tableInfos, author, baseType, classNamePre, packageName, mapperOgnl, mapperSuffix, strict,output);
		} catch (Exception e) {
			e.printStackTrace();
		}finally {
			CloseUtil.close(output);
		}
	}
	
	/**
	 * WEB服务生成代码
	 * 
	 * @param response
	 * @param check			1校验，0生成
	 * @param db			数据库
	 * @param tableName		表名：* 表示全部，xx* 前缀匹配，xx 单表，xx,yy 多表
	 * @param author		作者
	 * @param baseType		根类型："" 普通数据 | Tree 树型数据 | Report 报表数据
	 * @param classNamePre	类名前缀，区分多数据源的同名表："" | Xxx
	 * @param packageName	包名前缀：cn.xxx.xxx
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年7月3日
	 */
	public static Result webGen(HttpServletResponse response, String check, DbGenParam db,String tableName,String author,String baseType,String classNamePre,String packageName) {
		return webGen(response, check, db, tableName, author, baseType, classNamePre, packageName,MAPPER_OGNL, MAPPER_SUFFIX);
	}
	
	/**
	 * WEB服务生成代码
	 * 
	 * @param response
	 * @param check		1校验，0生成
	 * @param db			数据库
	 * @param tableName		表名：* 表示全部，xx* 前缀匹配，xx 单表，xx,yy 多表
	 * @param author		作者
	 * @param baseType		根类型："" 普通数据 | Tree 树型数据 | Report 报表数据
	 * @param classNamePre	类名前缀，区分多数据源的同名表："" | Xxx
	 * @param packageName	包名前缀：cn.xxx.xxx
	 * @param mapperOgnl	Mapper.xml的OGNL类名：cn.demoncat.util.mybatis.util.MapperOgnl
	 * @param mapperSuffix	Mapper类的后缀：Dao | Mapper
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年7月3日
	 */
	public static Result webGen(HttpServletResponse response, String check, DbGenParam db,String tableName,String author,String baseType,String classNamePre,String packageName,String mapperOgnl,String mapperSuffix) {
		// 查询数据库表
		List<MysqlTable> tableInfos = DbGenUtil.listTableInfos(db, DbGenUtil.listTables(db, tableName));
		// 校验
		if (Common.YES.equals(check)) {
			if (tableInfos.size() > 0) {
				return MybatisCodeGenUtil.check(tableInfos);
			}else {
				return Result.error("数据库中没有可用的表");
			}
		}
		// 下载代码压缩包
		gen(db, tableInfos, author, baseType, classNamePre, packageName, mapperOgnl, mapperSuffix, true, ServletUtil.getStreamOut(response, db.getName() + ".zip"));
		// 下载成功返回null，避免SpringMvc二次输出响应
		return null;
	}
	
	/**
	 * 生成代码
	 *
	 * @param db			数据库信息
	 * @param tableInfos	表信息
	 * @param author		作者
	 * @param baseType		根类型：DbGenConstant.BaseType
	 * @param classNamePre	类名前缀，区分多数据源的同名表："" | Xxx
	 * @param packageName	包名前缀：cn.xxx.xxx
	 * @param mapperOgnl	Mapper.xml的OGNL类名：cn.demoncat.util.mybatis.util.MapperOgnl
	 * @param mapperSuffix	Mapper类的后缀：Dao | Mapper
	 * @param strict		严格的：校验id、注释
	 * @param out			输出流
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年7月3日
	 */
	private static void gen(DbGenParam db, List<MysqlTable> tableInfos,String author,String baseType,String classNamePre,String packageName,String mapperOgnl,String mapperSuffix,boolean strict, OutputStream out) {
		// Ognl,MapperSuffix
		if (StringUtils.isBlank(mapperOgnl)) {
			mapperOgnl = MAPPER_OGNL;
		}
		if (StringUtils.isBlank(mapperSuffix)) {
			mapperSuffix = MAPPER_SUFFIX;
		}
		// 创建结果目录
		String resultDir = TMP_DER + System.currentTimeMillis();
		File resultFile = new File(resultDir);
		// 生成代码
		try {
			generate(db, tableInfos, author, baseType, classNamePre, packageName, resultFile, resultDir, mapperOgnl, mapperSuffix, strict, out);
		}finally {
			// 删除结果文件
			FileUtil.delFile(resultFile);
		}
	}
	
	/**
	 * 生成代码校验
	 * 
	 * @param tableInfos	表信息
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年7月3日
	 */
	private static Result check(List<MysqlTable> tableInfos) {
		// 遍历表信息
		boolean hasId = false;
		for (MysqlTable tableInfo : tableInfos) {
			// 表必须有注释
			if (StringUtils.isBlank(tableInfo.getComment())) {
				return Result.error(tableInfo.getName() +" 表没有注释");
			}
			for (MysqlColumn column : tableInfo.getColumns()) {
				// 字段必须有注释
				if (StringUtils.isBlank(column.getComment())) {
					return Result.error(tableInfo.getName() +" 表的 "+column.getName()+" 字段没有注释");
				}
				// 创建属性
				if (column.getName().equals("id")) {
					hasId = true;
				}
			}
			if (!hasId) {
				return Result.error(tableInfo.getName() +" 表缺少 id 字段");
			}
		}
		return Result.SUCCESS;
	}
	
	/**
	 * 生成代码
	 *
	 * @param db
	 * @param tableInfos
	 * @param author
	 * @param baseType
	 * @param packageName	根包名
	 * @param resultFile
	 * @param resultDir		结果目录名
	 * @param mapperOgnl	OGNL类名：cn.demoncat.util.mybatis.util.MapperOgnl
	 * @param mapperSuffix	Mapper类的后缀：Dao | Mapper
	 * @param strict		严格的：校验id、注释
	 * @param out			输出流
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年7月3日
	 */
	private static void generate(DbGenParam db, List<MysqlTable> tableInfos,String author,String baseType,String classNamePre, String packageName,File resultFile,String resultDir,String mapperOgnl,String mapperSuffix,boolean strict, OutputStream out) {
		// 结果目录
		String entityDir = resultDir + "/pojo/";
		String daoDir = resultDir + "/mapper/";
		String mapperDir = resultDir + "/mapper-xml/";
		String serviceDir = resultDir + "/service/";
		String serviceImplDir = resultDir + "/service-impl/";
		String webDir = resultDir + "/web/";
		String restDir = resultDir + "/rest/";
		String feignDir = resultDir + "/feign/";
		String servicePlusDir = resultDir + "/plus-service/";
		String serviceImplPlusDir = resultDir + "/plus-service-impl/";
		String webPlusDir = resultDir + "/plus-web/";
		new File(entityDir).mkdirs();
		new File(daoDir).mkdirs();
		new File(mapperDir).mkdirs();
		new File(serviceDir).mkdirs();
		new File(serviceImplDir).mkdirs();
		new File(webDir).mkdirs();
		new File(restDir).mkdirs();
		new File(feignDir).mkdirs();
		new File(servicePlusDir).mkdirs();
		new File(serviceImplPlusDir).mkdirs();
		new File(webPlusDir).mkdirs();
		// 包名
		String entityPackage = packageName + ".pojo";
		String mapperPackage = packageName + ".mapper";
		String servicePackage = packageName + ".service";
		String serviceImplPackage = packageName + ".service.impl";
		String webPackage = packageName + ".web";
		String restPackage = packageName + ".rest";
		String feignPackage = packageName + ".feign";
		// baseType
		BaseType bt = BaseType.get(baseType);
		// 遍历表信息
		DbGenTable table;
		DbGenField field;
		List<DbGenField> fields;
		DbGenEntity entity;
		DbGenAttr attr;
		List<DbGenAttr> attrs;
		boolean hasId = false;
		for (MysqlTable tableInfo : tableInfos) {
			LOG.info("开始生成 "+ tableInfo.getName() +" 表的代码 ......");
			// 表必须有注释
			if (strict && StringUtils.isBlank(tableInfo.getComment())) {
				throw new RuntimeException(tableInfo.getName() +" 表没有注释");
			}
			// 创建表
			table = new DbGenTable();
			table.setName(tableInfo.getName());
			table.setJavaName(JDBCUtil.toJavaName(tableInfo.getName()));
			if (StringUtils.isBlank(tableInfo.getComment())) {
				table.setComment(StringConstant.EMPTY);
			}else{
				table.setComment(tableInfo.getComment().endsWith("表")?tableInfo.getComment().substring(0, tableInfo.getComment().length()-1):tableInfo.getComment());
			}
			// 创建实体类
			entity = new DbGenEntity();
			entity.setPackageName(entityPackage);
			entity.setComment(table.getComment());
			entity.setSimpleName(JDBCUtil.toJavaName(StringUtils.isBlank(classNamePre)? table.getName() : classNamePre.toLowerCase()+"_"+table.getName()));
			entity.setName(entity.getSimpleName().substring(0, 1).toUpperCase() + entity.getSimpleName().substring(1));
			entity.setFullName(entity.getPackageName() + "." + entity.getName());
			entity.setUrl(StringConstant.SEPARATOR + StringUtils.replace(table.getName().toLowerCase(), StringConstant.UNDER_LINE, StringConstant.SEPARATOR));
			entity.setAuthor(author);
			entity.setTime(LocalDateUtil.toDateStr());
			entity.setBasePackage(BASE_PACKAGE);
			entity.setBaseType(bt.getBaseType());
			// 创建表字段和属性
			fields = new ArrayList<>();
			attrs = new ArrayList<>();
			for (MysqlColumn column : tableInfo.getColumns()) {
				// 字段必须有注释
				if (strict && StringUtils.isBlank(column.getComment())) {
					throw new RuntimeException(tableInfo.getName() +" 表的 "+column.getName()+" 字段没有注释");
				}
				// 创建表字段
				field = new DbGenField();
				field.setName(column.getName());
				field.setComment(column.getComment());
				field.setJavaName(JDBCUtil.toJavaName(column.getName()));
				field.setType(column.getType());
				field.setJdbcType(JDBCUtil.getJdbcType(column.getType()));
				fields.add(field);
				// 创建属性
				if (field.getName().equals("id")) {
					hasId = true;
				}
				attr = new DbGenAttr();
				attr.setField(field);
				attr.setName(JDBCUtil.toJavaName(field.getName()));
				attr.setComment(field.getComment());
				attr.setType(JDBCUtil.getJavaType(field.getType()));
				attr.setPoInclude(!ArrayUtils.contains(bt.getLinealAttrs(), attr.getName()));
				attrs.add(attr);
			}
			if (strict && !hasId) {
				throw new RuntimeException(table.getName() +" 表缺少 id 字段");
			}
			table.setFields(fields);
			entity.setAttrs(attrs);
			// 数据模型
			HashMap<String, Object> data = new HashMap<>();
			// 存入序列化值、名称
			data.put("table", table);
			data.put("entity", entity);
			data.put("mapper", new DbGenMapper(mapperPackage, mapperOgnl));
			data.put("service", new DbGenService(servicePackage));
			data.put("serviceImpl", new DbGenServiceImpl(serviceImplPackage));
			data.put("web", new DbGenWeb(webPackage));
			data.put("rest", new DbGenRest(restPackage));
			data.put("feign", new DbGenFeign(feignPackage, db.getName() + "-web"));
			data.put("mapperSuffix", mapperSuffix);
			// 使用模板生成文件
			generate(table.getName(), data, "entity.ftl", entityDir + entity.getName() + ".java");
			generate(table.getName(), data, "dao.ftl", daoDir + entity.getName() + mapperSuffix + ".java");
			generate(table.getName(), data, "mapper.ftl", mapperDir + entity.getName() + "Mapper.xml");
			generate(table.getName(), data, "service.ftl", serviceDir + entity.getName() + "Service.java");
			generate(table.getName(), data, "service-impl.ftl", serviceImplDir + entity.getName() + "ServiceImpl.java");
			generate(table.getName(), data, "web.ftl", webDir + entity.getName() + "Controller.java");
			generate(table.getName(), data, "rest.ftl", restDir + entity.getName() + "Rest.java");
			generate(table.getName(), data, "feign.ftl", feignDir + entity.getName() + "Feign.java");
			generate(table.getName(), data, "plus-service.ftl", servicePlusDir + entity.getName() + "Service.java");
			generate(table.getName(), data, "plus-service-impl.ftl", serviceImplPlusDir + entity.getName() + "ServiceImpl.java");
			generate(table.getName(), data, "plus-web.ftl", webPlusDir + entity.getName() + "Controller.java");
		}
		
		// 去除"\65279"编码错误
		try {
			cleanBom(resultFile);
		} catch (IOException e) {
			throw new IoRuntimeException("整理代码文件失败", e);
		}
		
		// 压缩输出
		ZipUtil.zip(resultFile, out);
	}
	
	/**
	 * 生成代码文件
	 * 
	 * @param tableName		表名
	 * @param data			数据
	 * @param template		模板名
	 * @param resultFile	结果文件路径
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年7月3日
	 */
	private static void generate(String tableName, HashMap<String, Object> data, String template, String resultFile) {
		try {
			FreeMarkerUtil.out(TEMPLATE_DIR, template, data, new FileOutputStream(resultFile));
		} catch (FileNotFoundException e) {
			throw new IoRuntimeException("生成 "+tableName+" 表的代码失败", e);
		}
	}
	
	/**
	 * 去除"\65279"编码错误 
	 *
	 * 问题：Eclipse生成文件时，会标记"\65279"头信息，导致代码运行时报错
	 * 解决：将文件转换为ISO-8859-1，然后删除首个乱码字符
	 */
	private static void cleanBom(File resultFile) throws IOException {
		// 读取的所有生成的文件
		Collection<File> files = FileUtils.listFiles(resultFile, null, true);
		for (File file : files) {
			// 读取文件
			String s = FileUtils.readFileToString(file, "ISO-8859-1");
			// 去除"\65279"
			s = StringUtils.strip(s, "\65279");
			s = StringUtils.strip(s, "ï»¿");
			// 写入文件
			FileUtils.write(file, s, "ISO-8859-1");
		}
	}

}
